#if defined(_WIN32)

#include "logger.h"
#include "watcher.h"


#include <Psapi.h>
#include <io.h>
#include <vector>

static int ProcessorNumber = 1;

static LONGLONG FileTimeToInteger(const FILETIME &ft) {
  LARGE_INTEGER ret;
  ret.HighPart = ft.dwHighDateTime;
  ret.LowPart = ft.dwLowDateTime;
  return ret.QuadPart;
}

Watcher &Watcher::Instance() {
  static std::unique_ptr<Watcher> iIns;
  if (!iIns) {
    iIns.reset(new Watcher);

    SYSTEM_INFO si;
    ::GetSystemInfo(&si);
    ProcessorNumber = (int)si.dwNumberOfProcessors;
  }

  return *iIns;
}

void Watcher::Breath() {
  static time_t nLastBreath = 0;

  time_t nTick = time(NULL);
  if (nTick == nLastBreath)
    return;
  nLastBreath = nTick;

  Json::Value iMsg;
  iMsg["action"] = "update";
  iMsg["watchers"] = Json::Value(Json::arrayValue);

  for (auto &kv : _mWatcher) {
    if (kv.second.emStatus != EStatus::Running)
      continue;
    Tail(kv.first, kv.second);

    HANDLE hProc =
        ::OpenProcess(PROCESS_QUERY_INFORMATION |
                          PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ,
                      false, kv.second.nPid);
    if (hProc == INVALID_HANDLE_VALUE) {
      CatchExit(kv.first, kv.second, EStatus::Fatal);
      continue;
    }

    DWORD nExitCode = 0;
    ::GetExitCodeProcess(hProc, &nExitCode);
    if (nExitCode != STILL_ACTIVE) {
      CatchExit(kv.first, kv.second,
                nExitCode == 0 ? EStatus::Stopped : EStatus::Fatal);
      continue;
    }

    PROCESS_MEMORY_COUNTERS pmc;
    ZeroMemory(&pmc, sizeof(PROCESS_MEMORY_COUNTERS));
    pmc.cb = sizeof(PROCESS_MEMORY_COUNTERS);

    Json::Value iState;
    iState["name"] = kv.first;
    iState["vsz"] = "-";

    ::GetProcessMemoryInfo(hProc, &pmc, sizeof(PROCESS_MEMORY_COUNTERS));
    iState["res"] = (int)pmc.PagefileUsage / 1024;

    FILETIME ftCreation, ftExit, ftKernel, ftUser, ftNow;
    ::GetSystemTimeAsFileTime(&ftNow);

    LONGLONG nNow = FileTimeToInteger(ftNow);
    LONGLONG nPeriod = nNow - kv.second.nLastCalcCPU;
    kv.second.nLastCalcCPU = nNow;

    if (::GetProcessTimes(hProc, &ftCreation, &ftExit, &ftKernel, &ftUser)) {
      LONGLONG nKernel = FileTimeToInteger(ftKernel);
      LONGLONG nUser = FileTimeToInteger(ftUser);
      LONGLONG nTotal = nKernel + nUser;
      LONGLONG nDiff = nTotal - kv.second.nLastCPUTotal;
      kv.second.nLastCPUTotal = nTotal;
      iState["cpu"] = std::to_string(nDiff * 1.f / nPeriod);
    } else {
      iState["cpu"] = "-";
    }

    ::CloseHandle(hProc);
    iMsg["watchers"].append(iState);
  }

  if (iMsg["watchers"].size() > 0)
    Notify("/ws", iMsg);
}

void Watcher::Start(const std::string &sName, const std::string &sPath,
                    const std::string &sCmd, int nRetry) {
  auto it = _mWatcher.find(sName);
  Info *pInfo = nullptr;

  if (it != _mWatcher.end() && it->second.emStatus == EStatus::Running)
    return;
  if (it == _mWatcher.end()) {
    Info iInfo;
    iInfo.nRetry = nRetry;
    iInfo.sCmd = sCmd;
    iInfo.sPath = sPath;

    _mWatcher[sName] = iInfo;
    pInfo = &_mWatcher[sName];
  } else if (it->second.emStatus == EStatus::Running) {
    return;
  } else {
    pInfo = &it->second;
  }

  PROCESS_INFORMATION pi;
  STARTUPINFOA si;
  SECURITY_ATTRIBUTES sa;

  ZeroMemory(&pi, sizeof(PROCESS_INFORMATION));
  ZeroMemory(&si, sizeof(STARTUPINFOA));

  sa.nLength = sizeof(SECURITY_ATTRIBUTES);
  sa.bInheritHandle = TRUE;
  sa.lpSecurityDescriptor = NULL;

  HANDLE hReader, hWriter;
  if (!::CreatePipe(&hReader, &hWriter, &sa, 0)) {
    LOG_ERR("Failed to create pipe for start watcher [%s]. Error: %d",
            sName.c_str(), ::GetLastError());
    return;
  }

  si.cb = sizeof(STARTUPINFOA);
  si.hStdError = hWriter;
  si.hStdOutput = hWriter;
  si.dwFlags |= STARTF_USESTDHANDLES;

  char pCmd[512] = {0};
  char pWorkDir[512] = {0};

  if (!sPath.empty()) {
    if (sPath[0] == '.') {
      ::GetFullPathNameA(sPath.c_str(), 512, pWorkDir, NULL);
    } else {
      strncpy(pWorkDir, sPath.c_str(), 512);
    }
  }

  if (sCmd[0] == '.') {
    std::string sExec;
    sExec.append(pWorkDir);
    sExec.append("\\");
    sExec.append(sCmd);

    std::vector<std::string> vSupportedSuffix;
    vSupportedSuffix.push_back(".exe");
    vSupportedSuffix.push_back(".bat");
    vSupportedSuffix.push_back(".cmd");

    bool bHasExtension = false;
    for (size_t i = sExec.size() - 1; i > 0; i--) {
      if (sExec[i] == '/' || sExec[i] == '\\')
        break;
      if (sExec[i] == '.') {
        std::string sExt = sExec.substr(i);
        if (std::find(vSupportedSuffix.begin(), vSupportedSuffix.end(), sExt) !=
            vSupportedSuffix.end())
          bHasExtension = true;
        break;
      }
    }

    if (!bHasExtension) {
      bool bFixup = false;

      for (auto const &sSuffix : vSupportedSuffix) {
        std::string sTest = sExec + sSuffix;
        if (_access(sTest.c_str(), 0) == 0) {
          sExec.append(sSuffix);
          bFixup = true;
          break;
        }
      }

      if (!bFixup) {
        LOG_ERR(
            "Start %s ... [Failed] Path : %s, Cmd: %s, Executable NOT FOUND",
            sName.c_str(), pWorkDir, sCmd.c_str());
        return;
      }
    }

    ::GetFullPathNameA(sExec.c_str(), 512, pCmd, NULL);
  } else {
    strncpy(pCmd, sCmd.c_str(), 512);
  }

  bool bOk = ::CreateProcessA(NULL, pCmd, NULL, NULL, TRUE, CREATE_NO_WINDOW,
                              NULL, pWorkDir, &si, &pi);

  ::CloseHandle(hWriter);

  if (!bOk) {
    ::CloseHandle(hReader);

    pInfo->emStatus = EStatus::Fatal;

    LOG_ERR("Start %s ... [Failed] Path : %s, Cmd: %s", sName.c_str(), pWorkDir,
            pCmd);
    return;
  }

  FILETIME ftNow;
  ::GetSystemTimeAsFileTime(&ftNow);

  pInfo->hPipe = hReader;
  pInfo->nPid = pi.dwProcessId;
  pInfo->nLastCalcCPU = FileTimeToInteger(ftNow);
  pInfo->nLastCPUTotal = 0;
  pInfo->emStatus = EStatus::Running;
  pInfo->nStart = time(NULL);

  LOG_INFO("Start %s ... [OK]. PID : %d", sName.c_str(), pInfo->nPid);

  Json::Value iMsg;
  iMsg["action"] = "status_change";
  iMsg["watcher"] = sName;
  iMsg["status"] = pInfo->emStatus;
  iMsg["pid"] = (int)pInfo->nPid;
  iMsg["start_time"] = pInfo->GetStartTime();

  AppendTail(sName, *pInfo, "\n\n----------- Start -----------\n");
  Notify("/ws", iMsg);
}

void Watcher::Stop(const std::string &sName) {
  auto it = _mWatcher.find(sName);
  if (it == _mWatcher.end() || it->second.emStatus != EStatus::Running)
    return;

  HANDLE hProc = ::OpenProcess(PROCESS_TERMINATE, FALSE, it->second.nPid);
  if (hProc == INVALID_HANDLE_VALUE) {
    LOG_ERR("Stop %s ... [Failed]", sName.c_str());
    return;
  }

  if (!::TerminateProcess(hProc, 0)) {
    ::CloseHandle(hProc);
    LOG_ERR("Stop %s ... [Failed]. Error: %d", sName.c_str(), ::GetLastError());
    return;
  }

  ::CloseHandle(hProc);
  CatchExit(sName, it->second, EStatus::Stopped);
  LOG_INFO("Stop %s ... [OK]", sName.c_str());
}

void Watcher::StopAll() {
  for (auto &kv : _mWatcher)
    Stop(kv.first);
}

void Watcher::Remove(const std::string &sName) {
  auto it = _mWatcher.find(sName);
  if (it == _mWatcher.end())
    return;

  if (it->second.emStatus == EStatus::Running)
    Stop(sName);
  _mWatcher.erase(it);
}

Watcher::Info *Watcher::Get(const std::string &sName) {
  auto it = _mWatcher.find(sName);
  if (it == _mWatcher.end())
    return nullptr;
  return &it->second;
}

void Watcher::Notify(const std::string &sScope, const Json::Value &rMsg) {
  if (_fNotifier)
    _fNotifier(sScope, rMsg);
}

void Watcher::Tail(const std::string &sName, Info &rInfo) {
  static char pLine[8192] = {0}, pData[8192] = {0};

  DWORD nSize = 0, nValid = 0;
  if (!::PeekNamedPipe(rInfo.hPipe, NULL, 0, &nSize, &nValid, NULL) ||
      nValid <= 0) {
    return;
  }

  memset(pLine, 0, 8192);
  memset(pData, 0, 8192);

  bool bOk = ReadFile(rInfo.hPipe, pLine, 8192, &nSize, NULL);
  if (bOk && nSize > 0) {
    int nRead = 0;
    for (int i = 0; i < (int)nSize; ++i) {
      if (pLine[i] == 0x1B && pLine[i + 1] == '[') {
        while (i < (int)nSize && pLine[i] != 'm')
          i++;
      } else {
        pData[nRead] = pLine[i];
        ++nRead;
      }
    }

    memcpy(rInfo.pTail + rInfo.nTail, pData, nRead);

    if (rInfo.nTail + nRead > 8192) {
      int nIdx = rInfo.nTail + nRead - 8192;
      int i = 0;

      for (; nIdx < rInfo.nTail + nRead; ++nIdx) {
        if (rInfo.pTail[nIdx] == '\n') {
          nIdx++;
          break;
        }
      }

      for (; i < rInfo.nTail + nRead - nIdx; ++i) {
        rInfo.pTail[i] = rInfo.pTail[i + nIdx];
      }

      rInfo.pTail[i] = '\0';
      rInfo.nTail = i;
    } else {
      rInfo.nTail += nRead;
    }

    Json::Value iMsg;
    iMsg["action"] = "tail_append";
    iMsg["watcher"] = sName;
    iMsg["data"] = pData;

    Notify("/tail/" + sName, iMsg);
    Notify("/tail_all", iMsg);
  }
}

void Watcher::AppendTail(const std::string &sName, Info &rInfo,
                         const std::string &sTail) {
  int nSize = sTail.size();
  memcpy(rInfo.pTail + rInfo.nTail, sTail.data(), nSize);

  if (rInfo.nTail + nSize > 8192) {
    int nIdx = rInfo.nTail + nSize - 8192;
    int i = 0;

    for (; nIdx < rInfo.nTail + nSize; ++nIdx) {
      if (rInfo.pTail[nIdx] == '\n') {
        nIdx++;
        break;
      }
    }

    for (; i < rInfo.nTail + nSize - nIdx; ++i) {
      rInfo.pTail[i] = rInfo.pTail[i + nIdx];
    }

    rInfo.pTail[i] = '\0';
    rInfo.nTail = i;
  } else {
    rInfo.nTail += nSize;
  }

  Json::Value iMsg;
  iMsg["action"] = "tail_append";
  iMsg["data"] = sTail;

  Notify("/tail/" + sName, iMsg);
}

void Watcher::CatchExit(const std::string &sName, Info &rInfo, EStatus emStatus,
                        bool bRetry /* = false */) {
  ::CloseHandle(rInfo.hPipe);

  rInfo.emStatus = emStatus;
  rInfo.nPid = -1;
  rInfo.nStart = 0;

  Json::Value iMsg;
  iMsg["action"] = "status_change";
  iMsg["watcher"] = sName;
  iMsg["status"] = rInfo.emStatus;
  iMsg["pid"] = (int)rInfo.nPid;
  iMsg["start_time"] = rInfo.GetStartTime();

  if (bRetry && rInfo.nRetry > 0) {
    rInfo.emStatus = EStatus::Retry;
    iMsg["status"] = EStatus::Retry;

    Notify("/ws", iMsg);
    AppendTail(sName, rInfo, "\n----------- Auto-retry -----------\n");

    Start(sName, rInfo.sPath, rInfo.sCmd, rInfo.nRetry - 1);
  } else {
    Notify("/ws", iMsg);
    AppendTail(sName, rInfo, "\n----------- Stopped -----------\n");
  }
}

#endif